들어가며

마틴 파울러가 쓴 리팩터링 책을 읽게 되었다. 코드를 중심으로 예시를 들어 이야기를 풀어나가는데, 처음에 나쁘지 않던 예시코드가 너무나 아름답게 변한 과정이 감탄스러웠다. 그래서 포스팅을 통해 정리 해본다.

  1. 초기코드
  2. 요구사항 소개
  3. 리팩터링
  4. 요구사항 적용

하는 네가지 단계로 정리해보았다.

1. 초기 코드

  • plays.json
1
2
3
4
5
{
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like It", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
  • Invoices.json
1
2
3
4
5
6
7
8
9
10
[
{
"customer": "BigCo",
"performances": [
{ "playID": "hamlet", "audience": 55 },
{ "playID": "as-like", "audience": 35 },
{ "playID": "othello", "audience": 40 }
]
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import INVOICE from "../invoices.json";
import PLAYS from "../plays.json";

function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역(고객명: ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;

for (let perf of invoice.performances) {
const play = plays[perf.playID]; // object {name, type}
let thisAmount = 0;
switch (play.type) {
case "tragedy": //비극
thisAmount = 40000;
if (perf.audience > 30) thisAmount += 1000 * (perf.audience - 30);
break;
case "comedy": //희극
thisAmount = 30000;
if (perf.audience > 20) thisAmount += 1000 + 500 * (perf.audience - 20);
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
// 포인트를 적립한다.
volumeCredits += Math.max(perf.audience - 30, 0);
// 희극 관객 5명마다 추가 포인트를 제공한다.
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

// 청구 내역을 출력한다.
result += `${play.name}: ${format(thisAmount / 100)} (${
perf.audience
}석)\n`;
totalAmount += thisAmount;
}
result += `총액: ${format(totalAmount / 100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}

statement(INVOICE[0], PLAYS);

결과

청구내역 (고객명: BicCo)
Hamlet: $650.00 (55석)
As You Like It: $580.00 (35석)
Othello: %500.00 (40석)

총액: $1,730.00
적립 포인트: 47점

나쁘지 않은 코드라는 생각이 들었다. 그러나 새로운 요구사항이 들어오면 이야기가 달라진다. 프로그램의 작동 방식을 더 쉽게 파악할 수 있도록 코드를 여러 함수와 프로그램 요소로 재구성한다. 그 뒤 기능을 추가하는 것이 훨씬 수월 한다.

프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다.

2. 추가 요구사항

1) HTML로 출력하라

  • 청구결과에 문자열을 추가하는 문장 각각을 조건문으로 감싸야 한다. => 복잡도가 크게 증가

2) 더 많은 장르를 커버

  • 공연료와 적립 포인트 계산법에 영향을 줄것 => 다양한 요구사항을 커버하기 힘들 수 있음

(+ 다른 사람이 읽고 이해해야할 일이 있는데 로직을 파악하기 어렵다면..)

3. 리팩터링

1) 함수추출(함수화) : 코드 분석 없이도 무슨 일인지 알 수 있도록!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
for (let perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;

//////////////////////////////////////////
// 아래 switch문은 totalAmount를 계산한다.
switch (play.type) {
case "tragedy": //비극
thisAmount = 40000;
if (perf.audience > 30) thisAmount += 1000 * (perf.audience - 30);
break;
case "comedy": //희극
thisAmount = 30000;
if (perf.audience > 20) thisAmount += 1000 + 500 * (perf.audience - 20);
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
//////////////////////////////////////////

// 포인트를 적립한다.
volumeCredits += Math.max(perf.audience - 30, 0);
// 희극 관객 5명마다 추가 포인트를 제공한다.
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

// 청구 내역을 출력한다.
result += `${play.name}: ${format(thisAmount / 100)} (${
perf.audience
}석)\n`;
totalAmount += thisAmount;
}

statement() 구문중 switch 코드는, totalAmount를 계산하고 있다. 함수로 추출하는 방식으로 앞서 파악한 정보를 코드에 반영한다. 코드가 하는 일을 설명하는 이름을 짓는다. amountFor(aPerformance)

  • 함수로 빼냈을때 유효 범위를 벗어나는 변수, 새 함수에서는 곧바로 사용할 수 없는 변수가 있는지 확인한다.

    perf, play는 값이 바뀌지 않으므로, 매개변수로 보내고, thisAmount는 값을 리턴한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function amountFor(perf, play) { // 값이 바뀌지 않는 변수는 매개변수로 전달
let thisAmount = 0;

switch (play.type) {
case "tragedy": //비극
thisAmount = 40000;
if (perf.audience > 30) thisAmount += 1000 * (perf.audience - 30);
break;
case "comedy": //희극
thisAmount = 30000;
if (perf.audience > 20) thisAmount += 1000 + 500 * (perf.audience - 20);
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
return thisAmount;
}

2) 변수의 이름을 더 명확하게 바꿔보기

thisAmountresult로 변경하여, 변수의 역할을 쉽게 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function amountFor(perf, play) { // 값이 바뀌지 않는 변수는 매개변수로 전달
let result = 0;

switch (play.type) {
case "tragedy": //비극
result = 40000;
if (perf.audience > 30) result += 1000 * (perf.audience - 30);
break;
case "comedy": //희극
result = 30000;
if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
return result;
}

3) 임시변수를 질의함수로 바꾸기

4) 변수 인라인화

play변수 제거하기 perf는 for문을 통해 매번 바뀌지만, play변수는 perf 를 통해 얻기 때문에 매개변수로 전달할 필요가 없다. 이런 임시 변수를 제거하면 로컬 범위에 존재하는 이름이 줄어 들어, 추출작업이 편해진다.

1
2
3
function playFor(aPerf) {
return plays[aPerf.playID]
}

statement내의 중첩함수로 선언하면, 함수 스코프 밖의 plays를 참조할 수 있다.

외부에서 변수가 변하는 것이 결과에 영향을 주지 않는 순수함수형식도 고려하여 짜면 좋겠다. plays는 다행히 값이 변하지 않기 때문에, 변수로 넘기지 않고 상위 스코프에서 가져다써도 무관할듯 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function statement(invoice, plays) {
// plays는 값이 변하지 않기 때문에, 변수로 굳이 넘기지 않았다.
function playFor(aPerf) {
return plays[aPerf.playID]
}

function amountFor(perf) { // play를 제거했다.
let result = 0;

// 변수를 인라인화
switch (playFor(perf).type) {
case "tragedy":
result = 40000;
if (perf.audience > 30) result += 1000 * (perf.audience - 30);
break;
case "comedy":
result = 30000;
if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(perf).type}`);
}
return result;
}

let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역(고객명: ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;

for (let perf of invoice.performances) {
// const play = plays[perf.playID]; => 인라인된 변수를 제거!!
// let thisAmount = amountFor(perf, playFor(perf))

volumeCredits += Math.max(perf.audience - 30, 0);

// if ("comedy" === play.type) {
if ("comedy" === playFor(perf).type) {
volumeCredits += Math.floor(perf.audience / 5);
}

// 변수를 인라인화
//result += `${play.name}: ${format(thisAmount / 100)} (${perf.audience}석)\n`;
result += `${playFor(perf).name}: ${format(amountFor(perf) / 100)} (${perf.audience}석)\n`;
// totalAmount += thisAmount;
totalAmount += amountFor(perf);
}

result += `총액: ${format(totalAmount / 100)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}

statement(INVOICE[0], PLAYS);

5) 임시변수 제거 & 함수 이름 바꾸기

  • Format
1
2
3
4
5
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format;

임시변수는 자신의 스코프에서만 의미가 있어, 로직이 길어지며 함수추출도 어렵게 한다.

또한 format은 함수가 하는 일을 충분히 설명해주지 못한다.

1
2
3
4
5
6
7
function usd(num) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(num/100)
}
  • volumeCreditsFor
1
2
3
4
5
6
7
8
9
10
function volumeCreditsFor(perf) {
let result = 0
result += Math.max(perf.audience - 30, 0);

if ("comedy" === playFor(perf).type) {
result += Math.floor(perf.audience / 5);
}

return result
}
  • 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function statement(invoice, plays) {
...
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역(고객명: ${invoice.customer})\n`;

for (let perf of invoice.performances) {
// 포인트를 적립한다.
volumeCredits += volumeCreditsFor(perf);

result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`;
// totalAmount += thisAmount;
totalAmount += amountFor(perf);
}

result += `총액: ${usd(totalAmount)}\n`;
result += `적립 포인트: ${volumeCredits}점\n`;
return result;
}

statement(INVOICE[0], PLAYS);

6) 반복문 쪼개기

7) 연관 변수끼리 모으기(문장 슬라이드하기)

1
2
3
4
5
6
7
8
9
let volumeCredits = 0; // 변수 선언(초기화)을 반복문 앞으로 이동
for (let perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
}

for (let perf of invoice.performances) {
result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`;
totalAmount += amountFor(perf);
}

반복문을 쪼개서 성능이 느려지지 않을까 걱정할 수 있지만, 이 정도 중복은 성능에 미치는 영향이 미미할 때가 많다. 게다가 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서, 우리의 직관을 초월하는 결과를 내어준다.

하지만 리팩터링이 성능에 상당한 영향을 주기도한다. 그런 경우라도, 저자는 개의치 않고 리팩터링한다. 잘 다듬어진 코드라야 성능 개선 작업도 훨씬 수월하기 때문이다. 리팩터링 과정에서 성능이 크게 떨어졌다면, 리팩터링 후 시간을 내어 성능을 개선한다.

위에서 배운 3) 임시변수를 질의함수로 바꾸기 1) 함수로 추출 4) 변수 인라인화 도 함께 적용해보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function totalVolumeCredits() {
let result = 0;
for (let perf of invoice.performances) {
result += volumeCreditsFor(perf);
}
return result;
}

function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
result += amountFor(perf);
}
return result;
}
  • 결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
function statement(invoice, plays) {  
let result = `청구 내역(고객명: ${invoice.customer})\n`;

for (let perf of invoice.performances) {
result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}

result += `총액: ${usd(totalAmount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

function totalVolumeCredits() {
let result = 0;
for (let perf of invoice.performances) {
result += volumeCreditsFor(perf);
}
return result;
}

function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
result += amountFor(perf);
}
return result;
}

function usd(num) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(num/100)
}

function playFor(aPerf) {
return plays[aPerf.playID]
}

function volumeCreditsFor(perf) {
let result = 0
result += Math.max(perf.audience - 30, 0);

if ("comedy" === playFor(perf).type) {
result += Math.floor(perf.audience / 5);
}

return result
}

function amountFor(perf) {
let result = 0;

switch (playFor(perf).type) {
case "tragedy":
result = 40000;
if (perf.audience > 30) result += 1000 * (perf.audience - 30);
break;
case "comedy":
result = 30000;
if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${playFor(perf).type}`);
}
return result;
}
}

statement(INVOICE[0], PLAYS);

4. 요구사항 : html 렌더링 코드 추가

8) 단계 쪼개기

HTML 버전을 만들기 위해서, 1) 데이터 계산 단계 2) 렌더링 단계 로 나눈다.

우선 함수 추출하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function statement(invoice, plays) {  
return renderPlainText(invoice, plays);
}

function renderPlainText(invoice, plays) {
let result = `청구 내역(고객명: ${invoice.customer})\n`;

for (let perf of invoice.performances) {
result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}

result += `총액: ${usd(totalAmount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

function totalVolumeCredits() {...}
function totalAmount() {...}
function usd(num) {...}
function playFor(aPerf) {...}
function volumeCreditsFor(perf) {...}
function amountFor(perf) {...}
}

statement(INVOICE[0], PLAYS);

계산하는 로직을 분리해야 한다. 두단계 사이 중간 데이터 역할을 할 객체를 만들어서 renderPlainText() 에 인수로 전달한다.

그러면 invoicesplays 를 중간 데이터 구조로 옮기면, 계산 관련 코드는 전부 statement()로 모으고, renderPlainText()는 data매개변수로 전달된 데이터만 처리하게 만들 수 있다. 코드로 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function statement(invoice, plays) {  
const statementData = {
customer: invoice.customer,
performances: invoice.performances
};

return renderPlainText(statementData, plays);
}

function renderPlainText(data, plays) {
let result = `청구 내역(고객명: ${data.customer})\n`;

for (let perf of data.performances) {
result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}

result += `총액: ${usd(totalAmount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

function totalVolumeCredits() {...}
function totalAmount() {...}
function usd(num) {...}
function playFor(aPerf) {...}
function volumeCreditsFor(perf) {...}
function amountFor(perf) {...}
}

statement(INVOICE[0], PLAYS);

9) 함수 옮기기

playsFor(perf), amountFor(perf), volumeCreditsFor(perf) 도 중간데이터에서 직접 셋팅하게 수정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function statement(invoice, plays) {  
const statementData = {
customer: invoice.customer,
performances: invoice.performances.map(enrichPerformance)
};

return renderPlainText(statementData, plays);

function enrichPerformance(aPerf) {
const result = Object.assing({}, aPerf);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}

// 함수를 데이터 계산 단계로 이동시켰다.
function playFor(aPerf) {...}

function amountFor(perf) {
let result = 0;

// play를 바로 사용할 수 있다!
switch (perf.play.type) {
case "tragedy":
result = 40000;
if (perf.audience > 30) result += 1000 * (perf.audience - 30);
break;
case "comedy":
result = 30000;
if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${perf.play.type}`);
}
return result;
}

function volumeCreditsFor(perf) {
let result = 0
result += Math.max(perf.audience - 30, 0);

if ("comedy" === perf.play.type) {
result += Math.floor(perf.audience / 5);
}

return result
}
}


function renderPlainText(data, plays) {
let result = `청구 내역(고객명: ${data.customer})\n`;

for (let perf of data.performances) {
result += `${perf.play.name}: ${usd(perf.amount)} (${perf.audience}석)\n`;
}

result += `총액: ${usd(totalAmount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

function totalVolumeCredits() {
let result = 0;
for (let perf of invoice.performances) {
// volumeCredits 바로 사용
result += perf.volumeCredits;
}
return result;
}
function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
// amount 사용하도록 수정
result += perf.amount;
}
return result;
}
function usd(num) {...}
}

statement(INVOICE[0], PLAYS);

마지막으로 총합을 구하는 부분 까지 옮기자

1
2
3
4
5
6
7
8
9
10
function statement(invoice, plays) {  
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
...
function totalAmount(data) {...}
function totalVolumeCredits(data) {...}
}

그리고 이제 statement()에 필요한 데이터 처리에 해당하는 코드를 모두 별도 함수로 빼내자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays));
}

function createStatementData(invoice, plays) {
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(statementData);
result.totalVolumeCredits = totalVolumeCredits(statementData);
return result;

function enrichPerformance(perf) {...}
function playFor(perf) {...}
function usd(num) {...}
function volumeCreditsFor(perf) {...}
function amountFor(perf) {...}
function totalVolumeCredits(data) {...}
function totalAmount(data) {...}
}
  • HTML 버전을 렌더링 하는 코드를 추가해보자!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function htmlStatement(invoice, plays) {
return renderHtml(createStatementData(invoice, plays));
}

function renderHtml(data) {
let result = `<h1>청구 내역(고객명: ${data.customer})</h1>\n`;
result += `<table>\n`;
result += `<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>`;
for (let perf of data.performances) {
// 청구 내역을 출력한다.
result += `<tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td>`;
result += `<td>${usd(perf.amount)}</td></tr>\n`;
}
result += `</table>\n`;
result += `총액: ${usd(data.totalAmount)}\n`;
result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
return result;
}

// usd를 renderHtml()에서도 사용할 수 있도록 최상위로 옮김
function usd(num) { ... }

4. 요구사항 : 다양한 장르 커버

조건부 로직을 포함한 amountFor()와 volumeCreditsFor()를 호출하여 공연료를 계산하는데, 이 두 함수를 전용클래스로 옮기고, 다형성을 이용해서 조건문을 없애보자.

  • 원본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function createStatementData(invoice, plays) {
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(statementData);
result.totalVolumeCredits = totalVolumeCredits(statementData);
return result;

function enrichPerformance(aPerf) {
const result = Object.assing({}, aPerf);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}


function amountFor(perf) {
let result = 0;

switch (perf.play.type) {
case "tragedy":
result = 40000;
if (perf.audience > 30) result += 1000 * (perf.audience - 30);
break;
case "comedy":
result = 30000;
if (perf.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${perf.play.type}`);
}
return result;
}

function volumeCreditsFor(perf) {
let result = 0
result += Math.max(perf.audience - 30, 0);

if ("comedy" === perf.play.type) {
result += Math.floor(perf.audience / 5);
}

return result
}

function playFor(perf) {...}
function usd(num) {...}
function totalVolumeCredits(data) {...}
function totalAmount(data) {...}
}
  • PerformanceCalculator 클래스
1
2
3
4
5
6
class PerformanceCalculator {
constructor(aPerf, aPlay) {
this.performance = aPerf;
this.play = aPlay;
}
}
1
2
3
4
5
6
7
8
function enrichPerformance(aPerf) {
const calculator = new PerformanceCalculator(aPerf, playFor(aPerf));
const result = Object.assing({}, aPerf);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}

함수 옮기기 기법을 적용해서 계산기 클래스로 옮기자 기존의 performance, play 데이터를 this.performancethis.play로 바꿔준다. 그리고 조건문이 들어있는 amountFor()volumeCreditsFor() 함수도 클래스로 이동 시킨다.

this를 남발 하는 것보다, 만약 this의 값이 변화한다면 변수로 받아 사용하는 것이 코드의 이해에 더 좋을 것이라고 생각한다.(내 생각)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class PerformanceCalculator {
constructor(aPerf, aPlay) {
this.performance = aPerf;
this.play = aPlay;
}

get amount() {
let result = 0;

switch (this.play.type) {
case "tragedy":
result = 40000;
if (this.performance.audience > 30) result += 1000 * (this.performance.audience - 30);
break;
case "comedy":
result = 30000;
if (this.performance.audience > 20) result += 1000 + 500 * (perf.audience - 20);
result += 300 * this.performance.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${this.play.type}`);
}
return result;
}

get volumeCredits() {
let result = 0
result += Math.max(this.performance.audience - 30, 0);

if ("comedy" === this.play.type) {
result += Math.floor(this.performance.audience / 5);
}

return result
}
}
1
2
3
4
5
6
7
8
function enrichPerformance(aPerf) {
const calculator = new PerformanceCalculator(aPerf, playFor(aPerf));
const result = Object.assing({}, aPerf);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}

이제 다형성을 지원하게 만들어보자. 자바스크립트에서는 생성자가 서브클래스의 인스턴스를 반환할 수 없기 때문에 생성자를 팩터리 함수로 바꾸기를 적용한다.

10) 생성자를 팩터리 함수로 바꾸기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function enrichPerformance(aPerf) {
const calculator = createPerformanceCalculator(aPerf, playFor(aPerf));
const result = Object.assing({}, aPerf);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = volumeCreditsFor(result);
return result;
}

function createPerformanceCalculator(aPerf, aPlay) {
switch(aPlay.type) {
case 'tragedy': return new TragedyCalculator(aPerf, aPlay);
case 'comedy': return new ComedyCalculator(aPerf, aPlay);
}
}

11) 조건부 로직 다형성으로 바꾸기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PerformanceCalculator {
constructor(aPerf, aPlay) {
this.performance = aPerf;
this.play = aPlay;
}

get amount() {
throw new Error("서브클래스를 통해서 처리하도록 설계")
}

get volumeCredits() {
throw Math.max(this.performance.audience - 30, 0)
}
}

class TragedyCalculator extends PerformanceCalculator {
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}

class ComedyCalculator extends PerformanceCalculator {
get amount() {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}

get volumeCredits() {
return super.volumeCredits + Math.fllor(this.performance.audience / 5);
}
}

amountFor() volumeCreditsFor() 의 조건부 로직을 생성함수 하나로 옮겼다. 같은 타입의 다형성을 기반으로 실행되는 함수가 많을 수록 이렇게 구성하는 쪽이 유리하다!

결과 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function createStatementData(invoice, plays) {
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(statementData);
result.totalVolumeCredits = totalVolumeCredits(statementData);
return result;

function enrichPerformance(aPerf) {
const calculator = createPerformanceCalculator(aPerf, playFor(aPerf));
const result = Object.assing({}, aPerf);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}

function createPerformanceCalculator(aPerf, aPlay) {
switch(aPlay.type) {
case 'tragedy': return new TragedyCalculator(aPerf, aPlay);
case 'comedy': return new ComedyCalculator(aPerf, aPlay);
}
}

function playFor(perf) {...}
function usd(num) {...}
function totalVolumeCredits(data) {...}
function totalAmount(data) {...}
}

class PerformanceCalculator {
constructor(aPerf, aPlay) {
this.performance = aPerf;
this.play = aPlay;
}

get amount() {
throw new Error("서브클래스를 통해서 처리하도록 설계")
}

get volumeCredits() {
throw Math.max(this.performance.audience - 30, 0)
}
}

class TragedyCalculator extends PerformanceCalculator {
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30);
}
return result;
}
}

class ComedyCalculator extends PerformanceCalculator {
get amount() {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience - 20);
}
result += 300 * this.performance.audience;
return result;
}

get volumeCredits() {
return super.volumeCredits + Math.fllor(this.performance.audience / 5);
}
}

쓰인 기법들 정리

  1. 함수 추출
  2. 변수 이름 명확히 변경
  3. 임시변수를 질의함수로 바꾸기
  4. 변수 인라인화
  5. 함수 이름 바꾸기
  6. 반복문 쪼개기
  7. 연관 변수끼리 모으기
  8. 단계 쪼개기
  9. 함수 이동
  10. 생성자를 팩터리 함수로 바꾸기
  11. 조건부 로직 다형성으로 바꾸기

리팩터링을 크게 세단계로 나눠 진행했다.

  1. 원본함수를 중첩함수 여러 개로 나눴다.
  2. 단계 쪼개기를 적용해 계산코드와 출력 코드로 분리했다.
  3. 계산 로직을 다형성으로 표현했다.

좋은 코드를 가늠하는 기준은 분분하지만, “수정하기 쉬운 코드”야 말로 좋은 코드라고 저자는 생각한다. 오류없이 빠르게 수정할 수 있으며, 고객에게 필요한 기능을 더 빠르고 저렴한 비용으로 제공하도록 해준다.

Reference

리팩터링 2판: Chaper 01 - 마틴 파울러 저, 남기혁 옮김